ISUCON9 の予選に参加して敗退した話
自分と、 @fwarashi, @wataoka の3人で参加した。 記憶が曖昧だけど、Max が 4xxx or 5xxx 点で,最終は 29xx 点だった。予選敗退。
- python で参加
- ボトルネック部分を探すのは比較的スムーズにできた
- python が全然わかっていないことがばれた
python クソ、来年は golang 前提行くか… みたいな気分(python への完全なる責任転嫁)
すごい疲れたけどたのしかったです。
準備
練習
今回は事前にチームで練習をした。 ISUCON6 の問題を使って、環境構築やログ解析ツールの使い方をみんなで確認した。 おかげで、本番は全員がログ解析できる状態になっていたので良かった。
秘伝のやつ
必要そうな操作をまとめたスクリプトを事前に作っておいた。中身は以下のような感じ。
- /etc, /home を固めて手元にバックアップ
- バックアップから必要なファイルを取ってきて rsync デプロイを可能にする
- 各種サービスのログローテート & 再起動
- ログを手元に持ってきて解析レポート作成
レポジトリにまとめた https://github.com/cormoran/ISUCON-template
kataribe, wsgi_lineprof_reporter を自動実行するようにしたのが昨年からの改良点。 事前練習のときに昨年使ったスクリプトを参考にゼロから作り直しのだけど、出来がよくて去年の自分すごいなとか思っていた。
デプロイ時に、実行したユーザと時刻を slack に勝手に投げるようにしておいたのは割と良かった
本番
今回は全員リモートで参加。 wherebyを使ってボイスチャットとスクリーンシェア、slack 文字ベースの情報共有、HackMD でリアルタイムな思考メモ共有をした。
alibaba cloud にパスポートが要求されることがあるとの噂があったけど、特に何もなくインスタンス起動できた。
その後は、事前に用意した、最初にやることメモに従って淡々と環境構築。微妙に何かにはまってログ解析が動くまで 1 時間+α くらいかかった気がする。
使用言語は、全員が使えそうな python を選択。 golang も検討していたけど、事前練習で python 周りの確認を色々やったので、本番は思考停止 python にした。
僕は研究で python 使いまくってるので python ちょっとできると思い込んでいた。
(golang はちょっとできると未だに思い込んでいますが)
ログの分析
kataribe で nginx のログを見ていると、
GET /items/[0-9]+\.json
がすごいアクセスされていてちょっと重いGET /new_items/[0-9]+\.json
が割とアクセスされていて結構重いGET /users/transactions.json
が重い
あたりが見えたので、そのあたりを見ることにした
まずはアクセス回数が多い 1, 2 を見ると api_shipment_status
がすごい遅いのと、get_category_by_id
, get_user_simple_by_id
あたりの N+1 が悪そうだったので、2手に分かれて確認と対策をすることにした
話していた結論をまとめると
items
のapi_shipment_status
が重いのは仕方なさそう- うまく status をこちらで管理してアクセスやめられない?
- 冷静に見ると、実は条件分岐であまり呼ばれない
get_user
が軽くできるとうれしい
category
は更新されないのでキャッシュできるuser
は2箇所更新があるので、キャッシュとキャッシュ破棄が必要category
とuser
の取得はアプリ全体で使われているので特に効果が大きそう
category と user の対策で罠にはまりまくった
最初 @wataoka にお願いして、自分が後でサポートに入ったけど、2人共罠にはまった
罠1: category_id の型がいろいろ
id をキーとして全 category を持った辞書をグローバルに定義してキャッシュに使うよう実装してもらったら、Key Error
が出る…
手元で実行できる環境は用意しなかったのでデプロイデバッグしたけど、実はどっかで category 追加されてる?とか言う話にもなった
関数の引数で、category_id が int ではなく str で飛んでくる場合があることをエスパーして解決
- 中途半端に型が適当な言語はクソですね…
- golang ならこんなことは無い
- js なら key は常に str になるんだが(これはこれでクソ)
エラーメッセージを一字一句ちゃんと見ていたら id が文字列になってることにすぐ気づけたっぽい
>>> a = {}
>>> a[1]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 1
>>> a['1']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: '1'
この手のバグは pytorch の Tensor 型とかでも起こるので注意が必要
罠2: マルチプロセスでのグローバル変数の共有
やろうとしたのは user
のキャッシュで、user
は更新があるので、適切に排他制御してキャッシュをアプリ全体で共有しないといけない。
とりあえずアプリの複数台スケールは捨てるとして、 category 同様グローバル変数をキャッシュにして実装したら、ベンチマークが確率的に fail するようになった。
原因は、GIL 回避のために gunicorn は複数プロセスを立ち上げるので、グローバル変数はそのままでは共有できなくて、multiprocessing.Manager とか使わないと更新を伴うキャッシュには使えないよって話。
後から見返すとお前ほんとに python で機械学習やってんのか…? って感じだけど、本番中は本当に頭から消えていた…。
最近は行儀よく concurrent.futures の map に iterable 渡すとかしかやってないのでね…(つらい)。
本番中は、ロックを色々見直したけど結局良くわからなくなって、python 投げ捨てたい気分になりながら、供養として user_id だけを使ってる部分で get_user しないようにしつつ、get_user_simple_by_id
は N+1 に戻した。
この時点で割と時間を浪費して、問題の解決もしてないのでお通夜気分になった。
よくよく考えると memcached とかを高速化のためのキャッシュに使って色々やったことないなぁ…
DB のトランザクションの中で完全な排他制御がしたい場合に、外部のキャッシュをうまく使うのってできるんですか…(考えたけどできない気がした、また調べる)
golang でグローバル変数にキャッシュするにしても、mysql のトランザクションの分離レベル関連の話をあまり丁寧に考えたことがなくて、どのタイミングでキャッシュ更新すればいいのかなどの確信がないので、そのへんも再確認したいと思った。
/users/transactions.json
category
とuser
の修正と並列で確認
transactions
の方は api_shipment_status
が N+1 になっていてこっちは呼び出し回数的にもどうもヤバそう、という話をした
api_shipment_status
の結果をいい感じにアプリ側でトラッキングできないか仕様を確認- アプリ側で status の完全なトラッキングは無理
- 一部ならなんとかなりそうだけど実装重そう(done になったら以降変わらないことには気づけなかった)
- リクエストを並列で投げる?
みたいな話をしていたけど、このへんで自分はuser
のキャッシュバグに注力して完全におまかせになってしまった
色々やってもらっていたけど、users
関連のバグとバッティングしたりしたこともあって効果を確認できず、何やったかの詳細も把握できていない
最後の方は SQL の N+1 解消に取り組んでもらっていたようだけど、事後評価するとリクエストを並列でやる方を試してもらったほうが良かったらしい
line profiler をちゃんと見ると、api_shipment_status
の時間が他の2桁以上多いので、このあたりはもう少し冷静に判断すべきだったっぽい
キャンペーン
最初思考停止で 4 でベンチマーク動かしてもらったら得点が変わらなかったので、わからんなぁと放置していた
いくつかコードを修正した後、後半で @wataoka が 1,2,3,4 全部試したら、点数が爆上がりしてちょっとテンションが上がった
アプリを複数台構成に(しようとした
去年までの経験から、無理に複数台構成する必要はなくて、今回はしない、と決めていたんだけど、users のキャッシュを諦めたので、悲しくもアプリの並列化が可能になった
top 見てると、初期よりマシだけどまだ CPU が食い潰されてて、特にさっとできることもなくなった気分になっていたので、DB だけでも別サーバーに移しますか…、と気分で複数台に取り掛かる(残り50分位)(だめなやつ)
3台それぞれで独立したサービスが動くところまではさっとできたけど、その後はまりどころに全部はまった
nginx でロードバランスする -> App のポートが空いてない -> DB のユーザが localhost -> あ(虚無)
って感じで時間がなくなってきたので諦めて再起動試験 & ベンチガチャ
懲りずに何度かトライしたけど、fail 直後にベンチサーバーが重くなって0点終了になりかけたのですごく心臓に悪かった
transactions の方の手伝いに入ったら、リクエストを並列で投げる、くらいはできる時間があったかもしれない
複数台構成は、過去問で事前確認しておけば、はまらずにさっとできるか、すぐに固有の問題にはまって諦められるかもしれないので、準備が必要ですね
ふりかえり
去年に比べて、最初の方で落ち着いてログを解析したり、アプリ周りをしっかり見ることができたのは良かった気がする。
今回の ISUCON に限らず、誰がやります?お願いします、という感じで分担するんだけど、実は自分がやりたいので、詰まったときに割とすぐに仕事を奪い返す謎ムーブが多い。
- 競プロの経験からすると、この辺は実力が直交 or 拮抗 する相手じゃないとうまく行かない気がしている(本当?
- トラブルが好きすぎるので何かちょっとでも変なことが起こるとすぐ飛びつくのが悪い?
- 難しいなぁ
あと、自分は SQL がわからんので DB に頼らず全部アプリ側でなんとかしようとする思想が強い(雑感想)